/*
 * Copyright (c) 2009, Jan Pastor. All Rights Reserved.
 */

package sk.jp.commons.utils;

import java.io.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.Set;

import sk.jp.commons.exception.ApplicationException;

/**
 * File system utilities.
 */
public class FileUtil {

    private static Random random = new Random();

    //----------------------------------------------------------------------

    private FileUtil() {
    }

    /**
     * Creates a copy of source file, placed to destination file.
     * @param srcFile source file.
     * @param dstFile destination file, that will be created.
     * @throws IOException if some IO exception occurrs.
     */
    public static void copyFile(File srcFile, File dstFile)
                            throws IOException {
        //Read:
        if(!srcFile.canRead()){
            throw new IOException("Can't read source file (path: '"+srcFile.getAbsolutePath()+"')");
        }
        FileInputStream srcFis = null;
        byte[] data;
        try {
            srcFis = new FileInputStream(srcFile);
            data = new byte[ (int)srcFile.length() ];
            if( srcFis.read(data) != srcFile.length() ){
                throw new IOException("Load content of source file failed");
            }
        } finally {
            if(srcFis != null){
                try {
                    srcFis.close();
                }catch(Exception ignored){
                }
            }
        }
        //Make parent directories (of destination file) if not exists:
        File dstFileDir = dstFile.getParentFile();
        if(!dstFileDir.mkdirs()){
            if(!dstFileDir.exists()){
                throw new IOException("Can't create destination directory ('"
                                      +dstFileDir.getAbsolutePath()+"')");
            }
        }
        //Write:
        FileOutputStream dstFos = null;
        try {
            dstFos = new FileOutputStream(dstFile);
            dstFos.write(data);
        } finally {
            if(dstFos != null){
                try{
                    dstFos.close();
                }catch(Exception e){
                    //ignore
                }
            }
        }
    }

    /**
     * Renames file <code>FromFile</code> to new name specified by <code>toFile</code>,
     * and (if remains) deletes <code>fromFile</code>.
     * <p>If <code>toFile</code> exists before operation, it is deleted before.
     * @param fromFile source file to move (rename).
     * @param toFile destination file name.
     */
    public static void moveFile(File fromFile, File toFile){
        if(toFile.exists() && !toFile.delete()){
            throw new ApplicationException("Can't delete existing destination file",
                                           "sk.jp.jpeg-imageio.util.file.err.del",
                                           ExceptionUtils.paramsToMap(
                                               "toFile", toFile.getAbsolutePath()));
        }
        if(!fromFile.renameTo(toFile)){
            throw new ApplicationException("Can't rename (move) source file to destination file",
                                           "sk.jp.jpeg-imageio.util.file.err.move",
                                           ExceptionUtils.paramsToMap(
                                               "fromFile", fromFile.getAbsolutePath(),
                                               "toFile", toFile.getAbsolutePath()));
        }
        if(fromFile.exists() && !fromFile.delete()){
//            log.warn(LogMsg.toString("Can't delete remainding source file",
//                                     "fromFile", fromFile.getAbsolutePath()));
        }
    }

    /**
     * Returns unique file name (without creating them) for new temporary file,
     * located in user's default temporary directory.
     * @return unique file name.
     */
    public static File getTempFile(){
        String tempDirProp = "java.io.tmpdir";
        String prefix = "jpegimageio_";
        String extension = ".tmp";
        String userTempDir = System.getProperty(tempDirProp);

        File tempDir = new File(userTempDir);
        String tempFileName = prefix + random.nextInt() + extension;
        File tempFile = new File(tempDir, tempFileName);

        if(tempFile.exists()){
            //If already exists, try to create new name recursively
            tempFile = getTempFile();
        }

        return tempFile;
    }

    /**
     * Returns content of given input stream.
     *
     * @param inputStream input stream.
     * @return input stream content, never null.
     * @throws Exception
     * @deprecated This method can fail, use better implementation -
     * {@link #getBytesFromStream(InputStream)}.
     */
    public static byte[] getStreamContent(InputStream inputStream) throws Exception {
        if (inputStream == null)
            throw new NullPointerException("Parameter inputStream is null");
        //
        int available = inputStream.available();
        byte[] buf = new byte[available];
        int r = inputStream.read(buf, 0, available);
        if (r != available) {
            throw new Exception("Unsuccesfull read: Read bytes count (" + r
                                + ") doesn't equals to available bytes count ("
                                + available + ")");
        }
        return buf;
    }

    /**
     * Reads bytes from (file) input stream.
     * @param is
     * @return content of file.
     * @throws IOException
     */
    public static byte[] getBytesFromStream(InputStream is) throws IOException {
        long length = is.available();
        if (length > Integer.MAX_VALUE) {
            throw new IOException("Size of file is too big to read");
        }
        byte[] bytes = new byte[(int) length];

        int offset = 0;
        int numRead;
        while (offset < bytes.length
               && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
            offset += numRead;
        }

        if (offset < bytes.length) {
            throw new IOException("Read from file failed");
        }

        is.close();
        return bytes;
    }

    /**
     * Returns a list of all directories they are in one of given directory root
     * or its subdirectory (recursive scan).
     * @param dirRoots a set of root directory absolute paths. Paths that points
     * to non-directory file-system item are ignored silently.
     * @param cancellable cancellable object that can be checked for cancell operation.
     * @return a list of all contained directories or subset of files if
     * cancellable was cancelled.
     */
    public static List<File> getAllDirs(Set<String> dirRoots, Cancellable cancellable){
        Set<File> dirRootsSet = new HashSet<File>();
        for (String dirRootPath : dirRoots) {
            File dirRoot = new File(dirRootPath);
            if(!dirRoot.exists() || !dirRoot.isDirectory()){
                continue;
            }
            dirRootsSet.add(dirRoot);
        }
        return getAllSubdirs(dirRootsSet, cancellable);
    }

    /**
     * Returns a list of all directories they are in one of given directory root
     * or its subdirectory (recursive scan).
     * @param dirRoots a set of root directories. Files that points
     * to non-directory file-system item are ignored silently.
     * @param cancellable cancellable object that can be checked for cancell operation.
     * @return a list of all root and contained directories or subset of files if
     * cancellable was cancelled.
     */
    public static List<File> getAllSubdirs(Set<File> dirRoots, Cancellable cancellable){
        List<File> dirs = new LinkedList<File>(dirRoots);

        for (File dirRoot : dirRoots) {
            if(!dirRoot.exists() || !dirRoot.isDirectory()){
                continue;
            }

            if(cancellable != null && cancellable.isCancelled()){
                return dirs;
            }

            Set<File> subDirs = getSubdirs(dirRoot);
            List<File> dirsInSubdirs = getAllSubdirs(subDirs, cancellable);
            dirs.addAll(dirsInSubdirs);
        }
        return dirs;
    }

    /**
     * Returns subdirectories of given directory.
     * @param dir
     * @return subdirectories of given directory.
     */
    public static Set<File> getSubdirs(File dir) {
        File[] subDirsArray = dir.listFiles(new FileFilter() {
                                        public boolean accept(File pathname) {
                                            return pathname.isDirectory();
                                        }
                                    });
        Set<File> subDirs = CollectionUtils.itemsToSet(subDirsArray);
        return subDirs;
    }

    /**
     * Returns a list of all files from given directory.
     * @param dir directory.
     * @param filenameFilter
     * @return a list of files.
     */
    public static List<File> getFiles(File dir, FilenameFilter filenameFilter){
        File[] files = dir.listFiles(filenameFilter);
        return CollectionUtils.itemsToList(files);
    }

    /**
     * Returns file extension or null if file has not extension.
     * @param fileName
     * @return string after last '.' in file name, or null if file name
     * doesn't contains '.' or end with '.'.
     */
    public static String getFileExtension(String fileName){
        int i = fileName.lastIndexOf('.');
        if(i  < 0 || i == (fileName.length() - 1)){
            return null;
        }
        return fileName.substring(i + 1);
    }

    /**
     * File name filter that allows only files that have name ending with
     * one of given extension.
     * <p>This filter is not case sensitive.
     */
    public static class FileExtensionFilter implements FilenameFilter {
        private Collection<String> extensions;

        /**
         * Initialize filter.
         * @param extensions a set of allowed extensions, all in lower case.
         */
        public FileExtensionFilter(Collection<String> extensions) {
            this.extensions = extensions;
        }
        public boolean accept(File dir, String name) {
            String ext = getFileExtension(name);
            return ext != null && extensions.contains(ext.toLowerCase());
        }
    }

    /**
     * Builds unique identifier (key) of given file.
     * @param file
     * @return unique identifier (key) of given file.
     */
    public static Integer getFileId(File file) {
        Integer fileId = new Integer(file.getAbsolutePath().hashCode());
        return fileId;
    }

    /**
     * Compute hash code of content of given file.
     * @param file
     * @return hash code of content of given file.
     */
    public static Integer getFileHash(File file) {
        Integer fileHash = new Integer(new Long(file.lastModified() + file.length()).hashCode());
        return fileHash;
    }

}
